/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package se392.ateam2006.meetingmgmt;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.List;
import se392.ateam2006.authentication.GroupEntity;
import se392.ateam2006.meetingmgmt.meeting.Duration;
import se392.ateam2006.meetingmgmt.meeting.MeetingEntity;
import se392.ateam2006.resourcemgmt.ResourceMgmtLocal;
import se392.ateam2006.resourcemgmt.equipment.EquipmentEntity;
import se392.ateam2006.resourcemgmt.equipment.EquipmentShell;
import se392.ateam2006.resourcemgmt.participant.ParticipationEntity;
import se392.ateam2006.resourcemgmt.participant.exclusionset.ExclusionRangeEntity;
import se392.ateam2006.resourcemgmt.participant.exclusionset.TransientExclusionSetManager;
import se392.ateam2006.resourcemgmt.room.RoomEntity;
import se392.ateam2006.utils.Debug;

/**
 *SVN COMMIT COMMENT PLS2DELETE!
 * @author rob
 */
public class MeetingTimeResolver implements Serializable {

    private ResourceMgmtLocal rmbean;
    private GregorianCalendar defaultWorkingDayStart;
    private GregorianCalendar defaultWorkingDayEnd;
    private int defaultWorkingDayStartHour;
    private int defaultWorkingDayEndHour;
    private String defaultWorkingWeekDays;
    Collection<MeetingEntity> bookedMeetings;
    private ArrayList<MeetingCandidate> candidates;
    public static final int PASSIVE_PREFERENCE_SCORE = 2;
    public static final int PASSIVE_AVAILABLE_SCORE = 1;
    public static final int PASSIVE_EXCLUSION_SCORE = 0;
    public static final int ACTIVE_PREFERENCE_SCORE = 10;

    public MeetingTimeResolver(ResourceMgmtLocal rmb, String defaultWorkingWeekDays, Collection<MeetingEntity> bookedMeetings, GregorianCalendar defaultWorkingDayStart, GregorianCalendar defaultWorkingDayEnd) {
        this.rmbean = rmb;
        this.defaultWorkingDayStart = defaultWorkingDayStart;
        this.defaultWorkingDayEnd = defaultWorkingDayEnd;
        this.defaultWorkingWeekDays = defaultWorkingWeekDays;
        this.bookedMeetings = bookedMeetings;
    }

    /**
     * Create MeetingCandidate List
     * Crop by Active Ex Sets
     * Score by Active Pref Sets
     * Score by Passive Pref Sets
     * Score by non-preference sets
     * crop by initiator-authorised-room availablity
     * crop by equipment availibity
     * @param meetingId the ID of the meeting
     * @param semanticBlockSize the resolution of our booking system in minutes.
     * @return
     */
    public synchronized ArrayList<MeetingCandidate> resolveTimes(MeetingEntity meeting, int semanticBlockSize) {
        TransientExclusionSetManager tsm = new TransientExclusionSetManager(rmbean.getExclusionSetsForMeetingId(meeting.getId()), rmbean);


        this.candidates = this.createAllPossibleCandidates(meeting, semanticBlockSize);

        //Get the Active exclusions from the TSM
        ArrayList<ExclusionRangeEntity> exList = tsm.getExSetsForActiveParticipants(meeting);

        this.cropByActiveExclusionSets(candidates, exList);
        //this.scoreByActivePreferenceSets(candidates);
        //this.scoreByPassivePreferenceSets(candidates);
        this.cropByRoom(candidates);

//        this.cropByEquipmentAvailability(localMeeting);

        //    this.cropByMeetingLength(candidates, meeting, semanticBlockSize);

        this.trimByValidity(candidates);

        //this.trimByScore(candidates, meeting);

        return candidates;
    }

    private void trimByValidity(ArrayList<MeetingCandidate> theCandidates) {
        for (int i = 0; i < theCandidates.size(); i++) {
            MeetingCandidate temp = theCandidates.get(i);
            if (!temp.getValidity()) {
                theCandidates.remove(temp);
            }
        }
    }

    private void trimByScore(ArrayList<MeetingCandidate> theCandidates, MeetingEntity meeting) {
        int numActive = rmbean.getActiveParticipantCount(meeting);
        int activeThreshold = numActive * MeetingTimeResolver.ACTIVE_PREFERENCE_SCORE;

        int numPassive = rmbean.getMeetingParticipantCount(meeting) - numActive;
        int passiveThreshold = numPassive * MeetingTimeResolver.PASSIVE_PREFERENCE_SCORE;


        for (int i = 0; i < theCandidates.size(); i++) {
            MeetingCandidate temp = theCandidates.get(i);
            if (temp.getActiveScore() < activeThreshold) {
                theCandidates.remove(temp);
                continue;
            }
            if (temp.getPassiveScore() < passiveThreshold / 5) {
                theCandidates.remove(temp);
            }
        }
    }

    private ArrayList<MeetingCandidate> createAllPossibleCandidates(MeetingEntity me, int meetingOffset) {

        MeetingCandidate candidate;
        candidates = new ArrayList<MeetingCandidate>();
        Duration d = me.getDuration();
        Calendar meetingStartRange = me.getStartDateRange();
        Calendar meetingEndRange = me.getEndDateRange();

        //Create the first MC;
        MeetingCandidate possibleCandidate = new MeetingCandidate(me);
        possibleCandidate.setStart(me.getStartDateRange(), meetingOffset);
        if (me.getStartDateRange() == null) {
            Debug.print(9, "MTR : me.getStartDateRange() returns null!!!");
        }

        while (possibleCandidate.getEnd().before(me.getEndDateRange())) {
            //Checks for Week Time ETC 
            if (!candidateIsWorkingDay(possibleCandidate)) {
                possibleCandidate.setValidity(false);
            }
            if (possibleCandidate.getValidity()) {
                candidates.add(possibleCandidate);
            }

            MeetingCandidate ptr = possibleCandidate; //temporary pointer

            //Create new possibleCandidate
            possibleCandidate = new MeetingCandidate(me);
            possibleCandidate.setStart((GregorianCalendar) ptr.getEnd().clone(), meetingOffset);
        }


        return candidates;
    }

    private boolean candidateIsWorkingDay(MeetingCandidate mc) {
        //Set up start and end of working day
        defaultWorkingDayStartHour = defaultWorkingDayStart.get(Calendar.HOUR_OF_DAY);
        defaultWorkingDayEndHour = defaultWorkingDayEnd.get(Calendar.HOUR_OF_DAY);

        //Check is a working day
        String day = this.getMeetingDayOfWeek(mc);
        String workingDays[] = defaultWorkingWeekDays.split(" ");
        boolean isDayOfWorkingWeek = false;

        for (int i = 0; !isDayOfWorkingWeek && i < workingDays.length; i++) {
            if (day.equals(workingDays[i])) {
                isDayOfWorkingWeek = true;
                break;
            }
        }

        if (!isDayOfWorkingWeek) {
            return false;
        }

        //Check is within times for a working day
        //GregorianCalendar dayStarts = defaultWorkingDayStart;
        //GregorianCalendar dayEnds = defaultWorkingDayEnd;
        //dayStarts.set(mc.getStart().YEAR, mc.getStart().MONTH, mc.getStart().DATE);
        //dayEnds.set(mc.getEnd().YEAR, mc.getEnd().MONTH, mc.getEnd().DATE);

        //Set start to be correct time on this day.
        Calendar start = (Calendar) mc.getStart().clone();
        start.set(Calendar.HOUR_OF_DAY, defaultWorkingDayStartHour);
        start.set(Calendar.MINUTE, 0);
        start.set(Calendar.SECOND, 0);

        Calendar end = (Calendar) start.clone();
        end.set(Calendar.HOUR_OF_DAY, this.defaultWorkingDayEndHour);

        //At this point we know the candidate is a working day so just check to see if it falls between
        //the correct hours
        if (mc.getStart().getTime().compareTo(start.getTime()) >= 0) {
            if (mc.getEnd().getTime().compareTo(end.getTime()) <= 0) {
                return true;
            }
        }

        return false;

    //Check start time is within our working day.
    //if (dayCalendarBefore(mc.getStart(), dayStarts) || dayCalendarAfter(mc.getStart(), dayEnds)) {
    //    return false;
    //}
    //
    //if (dayCalendarBefore(mc.getEnd(), dayStarts) || dayCalendarAfter(mc.getEnd(), dayEnds)) {
    //    return false;
    //}
    //
    //return true;
    }

    boolean temp_print = false;
    private void cropByActiveExclusionSets(ArrayList<MeetingCandidate> theCandidates, ArrayList<ExclusionRangeEntity> theExclusions) {
        if(temp_print == false){
            Debug.printMC(theCandidates);
            Debug.print("==========================");
            Debug.printEX(theExclusions);
            temp_print = true;
        }
        for (MeetingCandidate mc : theCandidates) {
            
            for (ExclusionRangeEntity ex : theExclusions) {
                if (!mc.getValidity()) {
                    continue;
                }
                //Check if MC should be excluded by directly matching an exclusions set or by 
                // startin in the middle of an exclusion set
                if (mc.getStart().getTime().compareTo(ex.getStartDate().getTime()) >= 0) {
                    if (mc.getStart().getTime().before(ex.getEndDate().getTime())) {
                        mc.setValidity(false);
                    }
                }
                //Check that the Meeting Candidate doesn't overlap an exclusion by
                //starting before it and ending in the middle.
                if (mc.getEnd().getTime().after(ex.getStartDate().getTime()) && mc.getEnd().getTime().before(ex.getEndDate().getTime())) {
                    mc.setValidity(false);
                }
            }
        }
    }

//    private void cropByActiveExclusionSets(ArrayList<MeetingCandidate> theCandidates) {
//        MeetingEntity meeting = theCandidates.get(0).getMeeting();
//        TransientExclusionSetManager tsm =
//                new TransientExclusionSetManager(rmbean.getExclusionSetsForMeetingId(meeting.getId()), rmbean);
//        ArrayList<ExclusionRangeEntity> theExclusions = tsm.getExSetsForActiveParticipants(meeting);
//        if(theExclusions==null){
//            Debug.print(9,"cropByActiveExclusionSets : No Exclusion Sets Returned!");
//        }
//        for (MeetingCandidate mc : theCandidates) {
//            for (ExclusionRangeEntity ex : theExclusions) {
//                Debug.print(6,"Meeting "+meeting.getId());
//                if (!mc.getValidity()) {
//                    continue;
//                }
//                //Check if MC should be excluded by directly matching an exclusions set start time or by 
//                // starting in the middle of an exclusion set
//                if (mc.getStart().getTime().after(ex.getStartDate().getTime()) || mc.getStart().getTime().compareTo(ex.getStartDate().getTime()) == 0) {
//                    if (mc.getStart().getTime().before(ex.getEndDate().getTime())) {
//                        mc.setValidity(false);
//                    }
//                }
//                //Check that the Meeting Candidate doesn't overlap an exclusion by
//                //starting before it and ending in the middle.
//                if (mc.getEnd().getTime().after(ex.getStartDate().getTime()) && mc.getEnd().getTime().before(ex.getEndDate().getTime())) {
//                    mc.setValidity(false);
//                }
//            }
//        }
//    }

    //Loop through mc and score for the best preferred slots
    private void scoreByActivePreferenceSets(ArrayList<MeetingCandidate> theCandidates) {
        this.scorePreference(theCandidates, true);
    }

    //Loop through mc and score for the most preferred by passive participants
    private void scoreByPassivePreferenceSets(ArrayList<MeetingCandidate> mc) {
        this.scorePreference(mc, false);
    }

    //Called by the other scoring* functions, this consolidates the scoring system
    //into one function, removing repetition.
    private void scorePreference(ArrayList<MeetingCandidate> mc, boolean active) {
        MeetingEntity me = mc.get(0).getMeeting();
        int score;
        TransientExclusionSetManager tsm =
                new TransientExclusionSetManager(rmbean.getExclusionSetsForMeetingId(me.getId()), rmbean);
        ArrayList<ExclusionRangeEntity> pref;

        if (active) {
            pref = tsm.getPrefSetsForActiveParticipants(me);
            score = MeetingTimeResolver.ACTIVE_PREFERENCE_SCORE;
        } else {
            pref = tsm.getPrefSetsForPassiveParticipants(me);
            score = MeetingTimeResolver.PASSIVE_PREFERENCE_SCORE;
        }

        for (MeetingCandidate m : mc) {
            for (ExclusionRangeEntity p : pref) {
                if (m.getStart().getTime().compareTo(p.getStartDate().getTime()) >= 0) {
                    if (m.getStart().getTime().before(p.getEndDate().getTime())) {
                        if (active) {
                            m.addActiveScore(score);
                        } else {
                            m.addPassiveScore(score);
                        }
                    }
                }
            }
        }
    }

//Remove meeting times where a room cannot be assigned by our initiator (Requires room stuff in mc)
// checks permissions
// checks size
// check room is actually available
    /* Create a list of possible rooms (This shouldn't be in MC - read below)
     * Iterate and remove those for which we dont have permissions
     * Of the remaining, check capacity remove those too small
     * Order by those with smallest size.
     * Now we have a set of rooms that could possibly be booked, loop through our MC and check to 
     * see if the room is available at that time, if it is, add it to a list of possibleRooms, stored in MC
     */
    private void cropByRoom(ArrayList<MeetingCandidate> candidates) {
        MeetingEntity meeting = candidates.get(0).getMeeting();

        Collection<RoomEntity> roomList = rmbean.getAllRooms();
        ArrayList<RoomEntity> rooms = new ArrayList<RoomEntity>();

        List<GroupEntity> permsList = meeting.getInitiator().getPermissions();
        ArrayList<GroupEntity> perms = new ArrayList<GroupEntity>();

        int numParticipants = rmbean.getMeetingParticipants(meeting).size();

        Debug.print(4, "cropByRoom : Meeting has " + numParticipants + "Participants");

        rooms.addAll(roomList);
        perms.addAll(permsList);

        for (int i = 0; i < rooms.size(); i++) {
            RoomEntity re = rooms.get(i);
            if (!userCanBookRoom(re, perms)) {
                rooms.remove(re);
                continue;
            }

            if (re.getCapacity() < numParticipants) {
                rooms.remove(re);
            }

        }

        //Do some checks on our room list.
        if (rooms.size() < 1) {
            System.out.println("No Rooms available for this meeting!");
        } //how should we handle this event? 

        Debug.print(4, "cropByRoom : There are " + rooms.size() + " available rooms");
        //Loop through each meeting candidate and populate its local room list with a copy
        //of the one we created above. Then, check each room for each mc, if the room isn't available for whole
        // of the meeting then remove it from the MC's list.
        for (MeetingCandidate mc : candidates) {
            mc.setPossibleRooms((ArrayList<RoomEntity>) rooms.clone());
            for (RoomEntity re : mc.getPossibleRooms()) {
            //I'm not sure how to do this bit.
            //by leaving empty we assume that the room will always be available(
            //(Remember that MC's should all be working time/day by now)
            //Worst Case: Remove this inner loop, leave the clone() - rooms may get double booked.
            }
        }

    //Ok, were done, we've set up the possible rooms for the candidate, next we check for equipment stuff.

//        for (MeetingCandidate mc : candidates) {
//            boolean roomFound = true;
//            //Ok, we have less rooms than MC, so make this the primary loop condition. That way we wont run through all the MC for every room we can't use.
//            for (RoomEntity room : rooms) {
//                roomFound = true;
//                //check for permission
//                List<GroupEntity> permissions = meeting.getInitiator().getPermissions();
//                boolean hasPermission = false;
//                System.err.println("CAZ: list permissions " + permissions);
//                for (GroupEntity ge : permissions) {
//                    System.err.println("CAZ: permission " + ge.getGroupid());
//                    if (room.getOwner().equalsIgnoreCase(ge.getGroupid())) {
//                        hasPermission = true;
//                    }
//                }
//                if (hasPermission == false) {
//                    roomFound = false;
//                    break;
//                }
//                //check for size
//                int numParticipants = rmbean.getMeetingParticipants(meeting).size();
//                if (room.getCapacity() < numParticipants) {
//                    roomFound = false;
//                    break;
//                }
//
//                //check availablity
//                for (MeetingEntity me : bookedMeetings) {
//                    //check the meeting is using the room we're interested in
//                    if (!me.getRoom().equals(room)) {
//                        continue;
//                    }
//
//                    if ((me.getStartDate().after(mc.getStart())) && (me.getStartDate().before(mc.getEnd()))) {
//                        roomFound = false;
//                        break;
//                    }
//
//                    if ((me.get().after(mc.getStart())) && (me.getEndDate().before(mc.getEnd()))) {
//                        roomFound = false;
//                        break;
//                    }
//                }
//                //set the room if found
//                if (roomFound) {
//                    mc.setRoom(room);
//                    break;
//                }
//            }
//            //if no room found remove that candidate
//            if (mc.getRoom() == null) {
//                mc.setValidity(false);
//            }
//        }
    }

    private boolean userCanBookRoom(RoomEntity room, ArrayList<GroupEntity> perms) {
        for (GroupEntity ge : perms) {
            if (room.getOwner().equalsIgnoreCase(ge.getGroupid())) {
                return true;
            }

        }

        return false;
    }

    private ArrayList<EquipmentShell> checkForRentableEquip(ArrayList<EquipmentShell> equipmentList, MeetingCandidate mc, MeetingEntity meeting) {
        //check for rentable equipment
        Collection<RoomEntity> allRooms = rmbean.getAllRooms();
        //don't check the room which we already have
        allRooms.remove(mc.getRoom());
        for (EquipmentShell equipmentShell : equipmentList) {
            for (RoomEntity re : allRooms) {
                Collection<EquipmentEntity> rentableEquipment = rmbean.getEquipmentByRoomId(re);
                for (EquipmentEntity ee : rentableEquipment) {
                    //check dealing with the equipment you want
                    if (ee.getType().equals(equipmentShell.equipmentType)) {
                        continue;
                    }
//check if equipment is fixed or rentable
                    if (ee.isFixed() == true) {
                        continue;
                    }
//check that the user has permission to rent the equipment
                    boolean hasPermission = false;
                    List<GroupEntity> permissions = meeting.getInitiator().getPermissions();
                    for (GroupEntity ge : permissions) {
                        if (ge.getGroupid().equalsIgnoreCase(re.getOwner())) {
                            hasPermission = true;
                        }

                    }
                    if (hasPermission == false) {
                        continue;
                    }
//Check equipment is available at current time
                    boolean equipmentAvailable = true;
                    for (MeetingEntity me : bookedMeetings) {
                        //check the meeting is using the room we're interested in
                        if (!me.getRoom().getRoomID().equals(ee.getRoomId())) {
                            continue;
                        }

                        if ((me.getStartDate().after(mc.getStart())) && (me.getStartDate().before(mc.getEnd()))) {
                            equipmentAvailable = false;
                            break;
                        }

                        if ((me.getEndDate().after(mc.getStart())) && (me.getEndDate().before(mc.getEnd()))) {
                            equipmentAvailable = false;
                            break;
                        }

                    }
                    if (equipmentAvailable) {
                        equipmentShell.count--;
                        if (equipmentShell.count == 0) {
                            equipmentList.remove(equipmentShell);
                        }

                    }
                }
            }
        }
        return equipmentList;
    }

    private ArrayList<EquipmentShell> evaluateEquip(String equipment) {
        String[] equipmentArray = equipment.split(",");
        ArrayList<EquipmentShell> equipmentList = new ArrayList<EquipmentShell>();
        for (int i = 0; i <
                equipmentArray.length; i++) {
            boolean equipmentAdded = false;
            while (equipmentList.iterator().hasNext()) {
                EquipmentShell temp = equipmentList.iterator().next();
                if (temp.equipmentType.equalsIgnoreCase(equipmentArray[i])) {
                    int indexOfCurrentEquip = equipmentList.indexOf(temp);
                    equipmentList.get(indexOfCurrentEquip).count++;
                    equipmentAdded =
                            true;
                }

            }
            if (equipmentAdded == false) {
                equipmentList.add(new EquipmentShell(equipmentArray[i], 1));
            }

        }
        return equipmentList;
    }

    private ArrayList<EquipmentShell> checkRoomEquipment(ArrayList<EquipmentShell> equipmentList, MeetingCandidate mc) {
        for (EquipmentShell equipmentShell : equipmentList) {
            //check if the room has equipment
            RoomEntity room = mc.getRoom();
            Collection<EquipmentEntity> roomEquipment = rmbean.getEquipmentByRoomId(room);
            if (roomEquipment != null) {
                for (EquipmentEntity equip : roomEquipment) {
                    if (equip.getType().equals(equipmentShell.equipmentType)) {
                        equipmentShell.count--;
                        if (equipmentShell.count == 0) {
                            equipmentList.remove(equipmentShell);
                            break;
                        }

                    }
                }
            }
        }
        return equipmentList;
    }

//Remove mc's where meeting is not available
    private void cropByEquipmentAvailability(MeetingEntity meeting) {
        String equipment = meeting.getEquipment();
        ArrayList<EquipmentShell> equipmentList = this.evaluateEquip(equipment);

        for (MeetingCandidate mc : candidates) {
            equipmentList = this.checkRoomEquipment(equipmentList, mc);
            //check if need to look for rentable equipment
            if (equipmentList.isEmpty()) {
                continue;
            }

            equipmentList = this.checkForRentableEquip(equipmentList, mc, meeting);
            if (equipmentList.isEmpty() == false) {
                mc.setValidity(false);
            }

        }
    }

    private void cropByMeetingLength(ArrayList<MeetingCandidate> mc, MeetingEntity meeting, int offset) {

        int minuteDuration = meeting.getDuration().getTimeInMinutes();
        int numCandidates = minuteDuration / offset;

        Debug.print(6, "cropByMeetingLength : \nminuteDuration = " +
                minuteDuration + "\nnumCandidates = " + numCandidates);

        Debug.print(6, "MC before cropyByMeetingLength");
        Debug.printMC(mc);

        this.trimByValidity(candidates); //Done here so that day rollovers work

        for (int i = 0; i < mc.size(); i++) {
            if (mc.get(i).getValidity()) {
                //Forward Check
                for (int j = i; j < i + numCandidates && mc.get(j) != null; j++) {
                    if (!mc.get(j).getValidity()) {
                        mc.get(i).setValidity(false);
                        break; //break out of J loop
                    }
                }
            }
        }
        Debug.print(6, "MC after cropyByMeetingLength");
        Debug.printMC(mc);
    }

    /**
     * returns a lowercase string of the day of week for the meeting candidate
     * based on the startDate
     * e.g "monday"
     */
    private String getMeetingDayOfWeek(MeetingCandidate mc) {
        String dayString;
        int dayInt = mc.getStart().get(Calendar.DAY_OF_WEEK);

        switch (dayInt) {
            case Calendar.MONDAY:
                dayString = "monday";
                break;
            case Calendar.TUESDAY:
                dayString = "tuesday";
                break;
            case Calendar.WEDNESDAY:
                dayString = "wednesday";
                break;
            case Calendar.THURSDAY:
                dayString = "thursday";
                break;
            case Calendar.FRIDAY:
                dayString = "friday";
                break;
            case Calendar.SATURDAY:
                dayString = "saturday";
                break;
            case Calendar.SUNDAY:
                dayString = "sunday";
                break;
            default:
                dayString = "error";
        }
        return dayString;
    }

    public boolean dayCalendarAfter(Calendar first, Calendar second) {
        return dayCalendarBefore(second, first);
    }

    public boolean dayCalendarBefore(Calendar first, Calendar second) {
        if (first == null) {
        //       Debug.print(5, "MTR : calendarBefore : First is null");
        }

        if (second == null) {
        //       Debug.print(5, "MTR : calendarBefore : Second is null");
        }

//Check hours
        if (first.get(Calendar.HOUR_OF_DAY) < second.get(Calendar.HOUR_OF_DAY)) {
            return true;
        }

//If hour is the same, check minutes
        if (first.get(Calendar.HOUR_OF_DAY) == second.get(Calendar.HOUR_OF_DAY)) {
            if (first.get(Calendar.MINUTE) <= second.get(Calendar.MINUTE)) {
                return true;
            }

        }

        return false;


    }

    private boolean calendarBefore(Calendar first, Calendar second) {
        if (first.get(Calendar.MILLISECOND) < second.get(Calendar.MILLISECOND)) {
            return true;
        }

        return false;
    }

    private boolean calendarAfter(Calendar first, Calendar second) {
        return this.calendarBefore(second, first);
    }
}
